return function(BOXUI)

local utils = BOXUI.utils

local int = utils.int
local max = utils.max
local clamp = utils.clamp
local newbox = utils.newbox
local setbox = utils.setbox
local includes = utils.includesPoint
local colorWhite = {1, 1, 1}

local M = utils.newclass("container")

M.template_theme =  {
	sliderThickness = 20, sliderLengthMin = 50,
	sliderBack = {0, 0.5, 0},
	sliderForeActive = {1, 0, 0}, sliderFore = {0.75, 0, 0},

	borderThickness = 8,
	borderBack = {0, 0, 0.5},
	borderFore = {0, 1, 1},
	
	handleHeight = 20
}

-- AREAS
local units = {}
M.units = units

-- Vertical Slider
units.SLIDER_V = newbox("sliderVert" )

units.SLIDER_V.drag = function(owner, x, y, dx, dy)
	local s = owner.boxes.sliderVert
	if s.dir ~= 0 then return end -- sliding
	local a_max = owner.viewYMax
	local a = owner.viewY
	local s_space = s.height - s.length
	
	a = clamp(a + int(dy * a_max / s_space), 0, a_max)
	if a ~= owner.viewY then
		owner.viewY = a
		s.pos = int((s_space * a) / a_max)
		owner.content:updateViewport(owner.viewX, owner.viewY, owner.viewW, owner.viewH)
		owner:redraw()
	end
end

units.SLIDER_V.update = function(owner, dt)
	local s = owner.boxes.sliderVert
	if s.dir == 0 then return end
	owner:slideTo( 0, s.dir, dt)
end

-- Horizontal Slider
units.SLIDER_H = newbox("sliderHori")

units.SLIDER_H.drag = function(owner, x, y, dx, dy)
	local s = owner.boxes.sliderHori
	if s.dir ~= 0 then return end -- sliding
	local a_max = owner.viewXMax
	local a = owner.viewX
	local s_space = s.width - s.length
	
	a = clamp(a + int(dx * a_max / s_space), 0, a_max)
	if a ~= owner.viewX then
		owner.viewX = a
		s.pos = int((s_space * a) / a_max)
		owner.content:updateViewport(owner.viewX, owner.viewY, owner.viewW, owner.viewH)
	end
end

units.SLIDER_H.update = function(owner, dt)
	local s = owner.boxes.sliderHori
	if s.dir == 0 then return end
	owner:slideTo(s.dir, 0, dt)
end

-- Handle
units.HANDLE = newbox("handle")

units.HANDLE.drag = function(owner, x, y, dx, dy)
	local handle = owner.boxes.handle
	local parent = owner.parent
	local pw, ph
	if parent then
		local box = (parent.boxes and parent.boxes.panel) or parent
		pw, ph = box.width, box.height
	else
		pw, ph = love.graphics.getDimensions()
	end
	owner.x = clamp(dx + owner.x, 0 - owner.width  + 20, pw - 20) 
	owner.y = clamp(dy + owner.y, 0, ph - handle.height) 
end

-- Handle Buttons
units.SHADE  = newbox("shade" )
units.SHADE.click = function(owner) owner:rollup() end

units.EXPAND = newbox("expand")
units.EXPAND.click = function(owner) owner:maximize() end

units.CLOSE  = newbox("close" )
units.CLOSE.click = function(owner)
	local parent = owner.parent
	if parent and parent.remove then
		parent:remove(parent:find(owner))
	end
end

-- Scalers
units.SCALE_N  = newbox("scaleN" )
units.SCALE_NE = newbox("scaleNE")
units.SCALE_NW = newbox("scaleNW")
units.SCALE_E  = newbox("scaleE" )
units.SCALE_W  = newbox("scaleW" )
units.SCALE_SE = newbox("scaleSE")
units.SCALE_SW = newbox("scaleSW")
units.SCALE_S  = newbox("scaleS" )

local scale = function(owner, x, y, dx, dy, xdir, ydir)
	
	owner.scaling = true
	local handle = owner.boxes.handle
	local parent = owner.parent
	local pw, ph
	local minw, minh = 150, 150
	local threshold = 20
	if parent then
		local box = (parent.boxes and parent.boxes.panel) or parent
		pw, ph = box.width, box.height
	else
		pw, ph = love.graphics.getDimensions()
	end
	
	local ownermax_x, ownermax_y = owner.x + owner.width, owner.y + owner.height
	-- fixme: this might be simplified
	if xdir < 0 then
		local x = clamp(owner.x + dx, 0, ownermax_x - minw)
		if x < pw - threshold then
			owner.width = clamp(owner.width - dx, minw, ownermax_x)
			owner.x = x
		end
	elseif xdir > 0 and ownermax_x + dx >= threshold then
		owner.width = clamp(owner.width + dx, minw, pw - owner.x)
	end

	if ydir < 0 then
		local y = clamp(owner.y + dy, 0, ownermax_y - minh)
		if y < ph - threshold then
			owner.height = clamp(owner.height - dy, minh, ownermax_y)
			owner.y = y
		end
	elseif ydir > 0 and ownermax_y + dy >= threshold then
		owner.height = clamp(owner.height + dy, minh, ph - owner.y)
	end
end

units.SCALE_N.drag  = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy,  0, -1) end
units.SCALE_NE.drag = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy,  1, -1) end
units.SCALE_NW.drag = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy, -1, -1) end
units.SCALE_E.drag  = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy,  1,  0) end
units.SCALE_W.drag  = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy, -1,  0) end
units.SCALE_SE.drag = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy,  1,  1) end
units.SCALE_SW.drag = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy, -1,  1) end
units.SCALE_S.drag  = function(owner, x, y, dx, dy) scale(owner, x, y, dx, dy,  0,  1) end

local scaleEnd = function(owner)
	owner.scaling = false
	owner.maxWidth, owner.maxHeight = 0, 0
	owner:refresh(true)  -- so force a rescale to owner.width, owner.height
end

units.SCALE_N.release,  units.SCALE_S.release  = scaleEnd, scaleEnd
units.SCALE_E.release,  units.SCALE_W.release  = scaleEnd, scaleEnd

units.SCALE_NE.release, units.SCALE_NW.release = scaleEnd, scaleEnd
units.SCALE_SE.release, units.SCALE_SW.release = scaleEnd, scaleEnd

-- Panel
units.PANEL   = newbox("panel")

units.PANEL.drag = function(owner, x, y, dx, dy)
	local content = owner.content
	x, y = owner:contentRelativePos(owner.mouseX, owner.mouseY)
	content:mousemoved(x, y, dx, dy)
end

units.PANEL.release = function(owner, x, y, button)
	x, y = owner:contentRelativePos(x, y)
	owner.content:mousereleased(x, y, button)
end

units.PANEL.update = function(owner, dt)
	local content = owner.content
	local update = content.update
	if update then update(content, dt) end
end

---------------- MODULE ----------------
M.init = function(self, x, y, width, height)
	self.theme = BOXUI.theme
	self.x, self.y = x, y
	self.width, self.height = width, height

	self.activeArea = nil
	self.selectedArea = nil
	
	self.mouseX, self.mouseY = 0, 0 -- updated by mousemoved
	self.mousepresses = {} -- for non button 1 clicks on content
	
	-- viewport of content
	self.viewX = 0
	self.viewY = 0
	self.viewH = 0
	self.viewW = 0
	self.viewYMax = 0
	self.viewXMax = 0
	
	-- visibility of elements
	self.hasHandle = false
	self.hasSliderVert = true
	self.hasSliderHori = true
	self.hasClose = false
	self.hasExpand = false
	self.hasShade = false
	self.movable = false
	self.scalable = false
	
	
	self.sliderAdvance = 50
	self.sliderAdvanceTime = 0.050
	self.sliderAdvanceInit = 0.250
	
	self.content = nil
	
	self.wrapVert = false -- adjust height to content
	self.wrapHori = false -- adjust width to content
	
	self.slideDown = false -- after refresh
	
	self.title = "unknown"
	
	self.units = utils.deepcopy(M.units)
	
	self.buffered = true
	--self.canvas  = love.graphics.newCanvas(width, height)
	self.maxWidth, self.maxHeight = width, height
	self.viewport = newbox("viewport", 0, 0, width, height)
end

--local dummyf = function() end
--M.expand = dummyf
--M.focus = dummyf

-- called by parent to update viewport
M.updateViewport = function(self, vpx, vpy, vpw, vph)
	setbox(self.viewport, vpx, vpy, vpw, vph)
end

-- redraw canvas
M.redraw = function(self)
	local canvas = self.canvas
	if not canvas then return end

	love.graphics.setCanvas(canvas)
	love.graphics.clear()
	self:drawPassive(0, 0)
	love.graphics.setCanvas()
end

-- draw either canvas or dynamically
M.draw = function(self, x, y)
	local theme = self.theme
	local vp = self.viewport
	x, y = self.x - vp.x + (x or 0), self.y - vp.y + (y or 0)

	if self.scaling then
		local w, h = self.width, self.height
		local bt = theme.borderThickness
		love.graphics.setColor(theme.borderBack)
		love.graphics.rectangle('fill', x, y, w, h)
		love.graphics.setColor(theme.borderFore)
		love.graphics.setLineWidth(bt)
		love.graphics.rectangle('line', x + bt / 2, y + bt / 2, w - bt, h - bt)
		love.graphics.print(("%i x %i"):format(w, h), x + bt, y + bt)
		return
	end
	
	if self.canvas then
		love.graphics.setColor(colorWhite)
		love.graphics.draw(self.canvas, x, y)
	else
		self:drawPassive(x, y)
	end

	self:drawActive(x, y)
	
	
end

-- create a new canvas if required, return max dimensions
M.fit = function(self)
	local selfw, selfh = self.width, self.height
	
	local canvas = self.canvas
	local buffered = self.buffered
	if buffered and not canvas then
		self.canvas = love.graphics.newCanvas(selfw, selfh)
		self.maxWidth, self.maxHeight = selfw, selfh
		return selfw, selfh
	end
	
	local width, height = self.maxWidth, self.maxHeight
	if selfw <= width and selfh <= height then return width, height end

	if canvas then canvas:release() end
	if buffered then
		self.canvas = love.graphics.newCanvas(selfw, selfh)
	end
	self.maxWidth, self.maxHeight = selfw, selfh
	return selfw, selfh
end

M.includes = includes

M.refresh = function(self, redraw)
	self.width, self.height = self:fit()
	self.activeArea = nil
	self.selectedArea = nil
	
	local theme = self.theme
	local st = theme.sliderThickness
	local bt = theme.borderThickness
	local hh = self.hasHandle and theme.handleHeight or 0
	local panelw = self.width  - 2 * bt
	local panelh = self.height - 2 * bt - hh

	local content = self.content
	if content.refresh then content:refresh() end
	local cwidth, cheight = content.width, content.height
	
	local stvert = self.hasSliderVert and st or 0
	local sthori = self.hasSliderHori and st or 0
	
	local vert, hori -- overflow check
	if cheight > panelh then
		vert = true
		panelw = panelw - stvert
	end
	if cwidth > panelw then
		hori = true
		panelh = panelh - sthori
		if not vert and cheight > panelh then
			vert = true
			panelw = panelw - stvert
		end
	end
	
	if vert then
		self.viewYMax = cheight - panelh
	else
		self.viewYMax = 0
		stvert = 0
	end
	
	if hori then
		self.viewXMax = cwidth - panelw
	else
		self.viewXMax = 0
		sthori = 0
	end
	
	if self.wrapVert and not vert then
		panelh = cheight
		self.height = panelh + 2 * bt + sthori + hh
	end
	
	if self.wrapHori and not hori then
		panelw = cwidth
		self.width = panelw + 2 * bt + stvert
	end
	
	if content.expand and (panelw > cwidth or panelh > cheight) then
		content:expand(panelw, panelh)
	end
	--content.x, content.y = bt, bt + hh
	content.parent = self
	if content.redraw then content:redraw() end
	
	cwidth, cheight = content.width, content.height

	-- compute boxes fixme: move to theme.lua?
	local u = self.units
	local boxes = {}
	local collidables = {}
	self.boxes = boxes
	self.collidables = collidables
	local box
	
	if self.scalable then 
		box = setbox(u.SCALE_SE, panelw + stvert, hh + panelh + sthori, 2 * bt, 2 * bt)
		boxes[box.name] = box; table.insert(collidables, box)
		box = setbox(u.SCALE_NW, 0, 0, 2 * bt, 2 * bt)
		boxes[box.name] = box; table.insert(collidables, box)
	end
	
	if self.hasHandle then
		local i = 1
		local handlew = panelw + stvert
		
		local hbw, hbh = theme.handleButtonWidth, theme.handleButtonHeight
		local hbx = bt + handlew
		local hby = bt + int((hh - hbh) / 2)
		local hbadvance = theme.handlePadVert + hbw

		if self.hasClose then
			hbx = hbx - hbadvance
			box = setbox(u.CLOSE, hbx, hby, hbw, hbh)
			boxes[box.name] = box; table.insert(collidables, box)
		end
		if self.hasShade then
			hbx = hbx - hbadvance
			box =setbox(u.SHADE, hbx, hby, hbw, hbh)
			boxes[box.name] = box; table.insert(collidables, box)
		end
		if self.hasExpand then
			hbx = hbx - hbadvance
			box = setbox(u.EXPAND, hbx, hby, hbw, hbh)
			boxes[box.name] = box; table.insert(collidables, box)
		end
		box = setbox(u.HANDLE, bt, bt, handlew, hh)
		boxes[box.name] = box
		if self.movable then table.insert(collidables, box) end
	end
	
	box = setbox(u.PANEL, bt, bt + hh, panelw, panelh)
	boxes[box.name] = box; table.insert(collidables, box)
	
	if vert and self.hasSliderVert then
		box = setbox(u.SLIDER_V, bt + panelw, bt + hh, st, panelh)
		box.length = max(theme.sliderLengthMin, int(panelh * (panelh / cheight)))
		box.pos = self.slideDown and (panelh - box.length) or 0
		box.dir = 0
		boxes[box.name] = box; table.insert(collidables, box)
	end
	
	if hori and self.hasSliderHori then
		box = setbox(u.SLIDER_H, bt, bt + hh + panelh, panelw, st)
		box.length = max(theme.sliderLengthMin, int(panelw * (panelw / cwidth)))
		box.pos, box.dir = 0, 0
		boxes[box.name] = box; table.insert(collidables, box)
	end
	
	self.viewX, self.viewY = 0, (self.slideDown and self.viewYMax or 0)
	self.viewW, self.viewH = (hori and panelw or cwidth), (vert and panelh or cheight)
	
	content:updateViewport(self.viewX, self.viewY, self.viewW, self.viewH)
	
	self.drawActive = self.theme.draw.containerActive
	self.drawPassive = self.theme.draw.containerPassive
	
	if redraw then self:redraw() end
end

M.selectActiveArea = function(self, x, y)
	local prevActive = self.activeArea
	local active = nil
	
	x, y = x - self.x, y - self.y

	local boxes = self.collidables
	for i, v in ipairs(boxes) do
		if not v.hidden and includes(v, x, y) then
			active = v
			break
		end
	end
	
	boxes = self.boxes
	local box = active
	if box then
		if box == boxes.sliderVert then
			local pos, boxpos = y - box.y, box.pos
			if pos < boxpos then box.dir = -1
			elseif pos >= boxpos + box.length then box.dir = 1
			else box.dir = 0 end
		elseif box == boxes.sliderHori then
			local pos, boxpos = x - box.x, box.pos
			if pos < boxpos then box.dir = -1
			elseif pos >= boxpos + box.length then box.dir = 1
			else box.dir = 0 end
		end
	end

	self.activeArea = active
	if prevActive ~= active then
		local content = self.content
		if prevActive == boxes.panel and content.unfocus then content:unfocus()
		elseif active == boxes.panel and content.focus   then content:focus() end
	end
end

M.contentRelativePos = function(self, x, y)
	local panel = self.boxes.panel
	return x - self.x - panel.x + self.viewX, y - self.y - panel.y + self.viewY
end

M.unfocus = function(self)
	local active = self.activeArea
	if not active then return end

	local content = self.content
	if active == self.boxes.panel and content.unfocus then content:unfocus() end
	self.activeArea = nil
end

M.wheelmoved = function(self, wx, wy)
	local x, y = self.mouseX, self.mouseY
	--if not self:includes(x, y) then return end
	
	local content = self.content
	local cwm = content.wheelmoved
	if self.activeArea == self.boxes.panel and cwm and cwm(content, wx, wy) then
		x, y = self:contentRelativePos(x, y)
		content:mousemoved(x, y, 0, 0)
		return
	end
	
	local viewymax = self.viewYMax
	if viewymax <= 0 then
		return 
	end -- nothing to scroll
	
	
	local viewy = int(self.viewY - wy * self.sliderAdvance)
	viewy = clamp(viewy, 0, viewymax)
	if viewy == self.viewY then
		return
	end
	
	self.viewY = viewy
	local s = self.boxes.sliderVert
	if s then
		s.pos = int((s.height - s.length) * (viewy / viewymax))
	end

	content:updateViewport(self.viewX, self.viewY, self.viewW, self.viewH)
	return true
end

M.mousemoved = function(self, x, y, dx, dy)
	self.mouseX, self.mouseY = x, y
	local selected = self.selectedArea
	local drag = selected and selected.drag
	local content = self.content
	
	if drag then
		self.dragging = true
		--if x > 0 and y > 0 then -- fixme: needed for scaling
			drag(self, x, y, dx, dy)
		--end
		return
	end
	
	if not self:includes(x, y) then return self:unfocus() end
	self:selectActiveArea(x, y)
	
	
	if self.activeArea == self.boxes.panel then
		x, y = self:contentRelativePos(x, y)
		content:mousemoved(x, y, dx, dy)
	end
end

M.mousepressed = function(self, x, y, button)
	self.dt = 0
	self.advancing = false
	
	if not self:includes(x, y) then return end
	
	if button == 1 then
		self.selectedArea = self.activeArea
	end
	
	if self.activeArea == self.boxes.panel then
		--print(self.id, "pressed", button, self.content.id)
		x, y = self:contentRelativePos(x, y)
		self.content:mousepressed(x, y, button)
		self.mousepresses[button] = true
	end
end


local shade_list = {
	panel = true, sliderVert = true, sliderHori = true,
	scaleN = true, scaleNW = true, scaleW = true, scaleSW = true,
	scaleS = true, scaleSE = true, scaleE = true, scaleNE = true,
	--close = false, expand = false, shade = false, handle = false,
}

M.rollup = function(self, shade)
	if shade == nil then shade = not self.shaded end
	local boxes = self.boxes
	local handle = boxes.handle
	if shade then
		self.shadedHeight = self.height
		self.height = handle.height + 2 * handle.x
	else
		self.height = self.shadedHeight
	end

	local s
	for k, v in pairs(shade_list) do
		s = boxes[k]
		if v and s then s.hidden = shade end
	end
	self.shaded = shade
	
	self:redraw()
end

local premaxvars = {x = true, y = true, width = true, height = true,
	maxWidth = true, maxHeight = true,
	canvas = true,
	wrapVert = true, wrapHori = true,
	scalable = true, movable = true}
	
M.maximize = function(self, maximize)
	if self.shaded then self:rollup(false) end
	if maximize == nil then maximize = not self.maximized end

	local old
	if maximize then
		old = {}
		self.preMaximize = old
		for k, v in pairs(premaxvars) do
			old[k] = self[k]
		end
	
		local minx, miny, maxw, maxh
		local parent = self.parent
		if parent then
			local box = (parent.boxes and parent.boxes.panel) or parent
			minx, miny = box.x, box.y
			maxw, maxh = box.width, box.height
		else
			minx, miny = 0, 0
			maxw, maxh = love.graphics.getDimensions()
		end
	
		self.x, self.y = minx, miny
		self.width, self.height = maxw, maxh
		self.maxWidth, self.maxHeight = 0, 0 -- force rescale during refresh -> fit
		self.scalable, self.movable = false, false
		self.wrapVert, self.wrapHori = false, false

	else
		old = self.preMaximize
		for k, v in pairs(premaxvars) do
			self[k] = old[k]
		end
		self.width, self.height = self.maxWidth, self.maxHeight
		self.maxWidth, self.maxHeight = 0, 0 -- force rescale during refresh -> fit
	end

	self.maximized = maximize
	
	self:refresh(true)
end

M.mousereleased = function(self, x, y, button)
	if button ~=1 then
		if self.mousepresses[button] then
			--print(self.id, "release", button, self.content.id)
			x, y = self:contentRelativePos(x, y)
			self.content:mousereleased(x, y, button)
		end
		return
	end
	
	local selected = self.selectedArea
	if selected == nil then return end

	self.selectedArea = nil

	if self.dragging then
		self.dragging = false
		local release = selected.release -- (drag) release
		if release then release(self, x, y, button) end
		
		if not self:includes(x, y) then return self:unfocus() end
		self:selectActiveArea(x, y)
		
		if self.activeArea == self.boxes.panel then
			x, y = self:contentRelativePos(x, y)
			self.content:mousemoved(x, y, 0, 0)
		end
		return
	end
	
	local click = selected.click
	if click and self.activeArea == selected  then return click(self) end
	
	if selected == self.boxes.panel then
		x, y = self:contentRelativePos(x, y)
		self.content:mousereleased(x, y, button)
	end
end

M.slideTo = function(self, xdir, ydir, dt)
	self.dt = self.dt + dt
	if not self.advancing then
		if self.dt < self.sliderAdvanceInit then return end
		self.advancing = true; self.dt = self.dt - self.sliderAdvanceInit
	end
	if self.dt < self.sliderAdvanceTime then return end
	self.dt = self.dt - self.sliderAdvanceTime
	local boxes = self.boxes
	local panel = boxes.panel

	local x, y = self.mouseX, self.mouseY
	x, y = x - self.x - panel.x, y - self.y - panel.y
	
	local s, a, a_len, a_max, targetpos
	dt = self.sliderAdvance -- constant time
	local changed = false
	if xdir ~= 0 then
		a     = int(self.viewX + xdir * dt)
		a_len = self.viewW
		a_max = self.viewXMax
		
		targetpos = int(clamp(x  + (x * a_max) / a_len - a_len / 2, 0, a_max))
		if xdir * (a - targetpos) > 0 then a = targetpos end
		
		if a ~= self.viewX then
			self.viewX = a
			s = boxes.sliderHori
			if s then
				s.pos = int(((s.width - s.length) * a) / a_max)
				if a == targetpos then s.dir = 0 end
			end
			changed = true
		end
	end
	
	if ydir ~= 0 then
		a     = int(self.viewY + ydir * dt) -- fixme use trim
		a_len = self.viewH
		a_max = self.viewYMax
		
		targetpos = int(clamp(y  + (y * a_max) / a_len - a_len / 2, 0, a_max))
		if ydir * (a - targetpos) > 0 then a = targetpos end
		
		if a ~= self.viewY then
			self.viewY = a
			s = boxes.sliderVert
			if s then
				s.pos = int(((s.height - s.length) * a) / a_max)
				if a == targetpos then s.dir = 0 end
			end
			changed = true
		end
	end
	
	if changed then
		self.content:updateViewport(self.viewX, self.viewY, self.viewW, self.viewH)
	end
end

M.update = function(self, dt)
	local active, selected = self.activeArea, self.selectedArea
	if not selected or selected ~= active or not selected.update then return end
	
	selected.update(self, dt)
end

BOXUI.add(M)
end

